import { AxiosProgressEvent, AxiosResponse } from 'axios'
import { Response } from './response'

declare module 'axios' {
  export interface AxiosProgressEvent {
    percentage: number | undefined
  }
}

export type DefaultInertiaConfig = {
  errorValueType: string
  sharedPageProps: PageProps
}
/**
 * Designed to allow overriding of some core types using TypeScript
 * interface declaration merging.
 *
 * @see {@link DefaultInertiaConfig} for keys to override
 * @example
 * ```ts
 * declare module '@inertiajs/core' {
 *   export interface InertiaConfig {
 *     errorValueType: string[]
 *     sharedPageProps: {
 *       auth: { user: User | null }
 *       flash: { success?: string; error?: string }
 *     }
 *   }
 * }
 * ```
 */
export interface InertiaConfig {}
export type InertiaConfigFor<Key extends keyof DefaultInertiaConfig> = Key extends keyof InertiaConfig
  ? InertiaConfig[Key]
  : DefaultInertiaConfig[Key]
export type ErrorValue = InertiaConfigFor<'errorValueType'>
export type SharedPageProps = InertiaConfigFor<'sharedPageProps'>

export type Errors = Record<string, ErrorValue>
export type ErrorBag = Record<string, Errors>

export type FormDataConvertibleValue = Blob | FormDataEntryValue | Date | boolean | number | null | undefined
export type FormDataConvertible =
  | Array<FormDataConvertible>
  | { [key: string]: FormDataConvertible }
  | FormDataConvertibleValue

export type FormDataType<T extends object> = {
  [K in keyof T]: T[K] extends infer U
    ? U extends FormDataConvertibleValue
      ? U
      : U extends (...args: unknown[]) => unknown
        ? never
        : U extends object | Array<unknown>
          ? FormDataType<U>
          : never
    : never
}

/**
 * Uses `0 extends 1 & T` to detect `any` type and prevent infinite recursion.
 */
export type FormDataKeys<T> = T extends Function | FormDataConvertibleValue
  ? never
  : T extends unknown[]
    ? ArrayFormDataKeys<T>
    : T extends object
      ? ObjectFormDataKeys<T>
      : never

/**
 * Helper type for array form data keys
 */
type ArrayFormDataKeys<T extends unknown[]> = number extends T['length']
  ? // Dynamic array
    | `${number}`
      | (0 extends 1 & T[number]
          ? never
          : T[number] extends FormDataConvertibleValue
            ? never
            : `${number}.${FormDataKeys<T[number]>}`)
  : // Tuple with known length
    | Extract<keyof T, `${number}`>
      | {
          [Key in Extract<keyof T, `${number}`>]: 0 extends 1 & T[Key]
            ? never
            : T[Key] extends FormDataConvertibleValue
              ? never
              : `${Key & string}.${FormDataKeys<T[Key & string] & string>}`
        }[Extract<keyof T, `${number}`>]

/**
 * Helper type for object form data keys
 */
type ObjectFormDataKeys<T extends object> = string extends keyof T
  ? string
  :
      | Extract<keyof T, string>
      | {
          [Key in Extract<keyof T, string>]: 0 extends 1 & T[Key]
            ? never
            : T[Key] extends FormDataConvertibleValue
              ? never
              : T[Key] extends any[]
                ? `${Key}.${FormDataKeys<T[Key]> & string}`
                : T[Key] extends Record<string, any>
                  ? `${Key}.${FormDataKeys<T[Key]> & string}`
                  : Exclude<T[Key], null | undefined> extends any[]
                    ? never
                    : Exclude<T[Key], null | undefined> extends Record<string, any>
                      ? `${Key}.${FormDataKeys<Exclude<T[Key], null | undefined>> & string}`
                      : never
        }[Extract<keyof T, string>]

type PartialFormDataErrors<T> = {
  [K in string extends keyof T ? string : Extract<keyof FormDataError<T>, string>]?: ErrorValue
}

export type FormDataErrors<T> = PartialFormDataErrors<T> & {
  [K in keyof PartialFormDataErrors<T>]: NonNullable<PartialFormDataErrors<T>[K]>
}

export type FormDataValues<T, K extends FormDataKeys<T>> = K extends `${infer P}.${infer Rest}`
  ? T extends unknown[]
    ? P extends `${infer I extends number}`
      ? Rest extends FormDataKeys<T[I]>
        ? FormDataValues<T[I], Rest>
        : never
      : never
    : P extends keyof T
      ? Rest extends FormDataKeys<T[P]>
        ? FormDataValues<T[P], Rest>
        : never
      : never
  : K extends keyof T
    ? T[K]
    : T extends unknown[]
      ? T[K & number]
      : never

export type FormDataError<T> = Partial<Record<FormDataKeys<T>, ErrorValue>>

export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'

export type RequestPayload = Record<string, FormDataConvertible> | FormData

export interface PageProps {
  [key: string]: unknown
}

export type ScrollProp = {
  pageName: string
  previousPage: number | string | null
  nextPage: number | string | null
  currentPage: number | string | null
  reset: boolean
}

export interface Page<SharedProps extends PageProps = PageProps> {
  component: string
  props: PageProps &
    SharedProps & {
      errors: Errors & ErrorBag
      deferred?: Record<string, VisitOptions['only']>
    }
  url: string
  version: string | null
  clearHistory: boolean
  encryptHistory: boolean
  deferredProps?: Record<string, VisitOptions['only']>
  mergeProps?: string[]
  prependProps?: string[]
  deepMergeProps?: string[]
  matchPropsOn?: string[]
  scrollProps?: Record<keyof PageProps, ScrollProp>

  /** @internal */
  rememberedState: Record<string, unknown>
}

export type ScrollRegion = {
  top: number
  left: number
}

export interface ClientSideVisitOptions<TProps = Page['props']> {
  component?: Page['component']
  url?: Page['url']
  props?: ((props: TProps) => PageProps) | PageProps
  clearHistory?: Page['clearHistory']
  encryptHistory?: Page['encryptHistory']
  preserveScroll?: VisitOptions['preserveScroll']
  preserveState?: VisitOptions['preserveState']
  errorBag?: string | null
  viewTransition?: VisitOptions['viewTransition']
  onError?: (errors: Errors) => void
  onFinish?: (visit: ClientSideVisitOptions<TProps>) => void
  onSuccess?: (page: Page) => void
}

export type PageResolver = (name: string) => Component

export type PageHandler<ComponentType = Component> = ({
  component,
  page,
  preserveState,
}: {
  component: ComponentType
  page: Page
  preserveState: boolean
}) => Promise<unknown>

export type PreserveStateOption = boolean | 'errors' | ((page: Page) => boolean)

export type QueryStringArrayFormatOption = 'indices' | 'brackets'

export type Progress = AxiosProgressEvent

export type LocationVisit = {
  preserveScroll: boolean
}

export type CancelToken = {
  cancel: VoidFunction
}

export type CancelTokenCallback = (cancelToken: CancelToken) => void

export type Visit<T extends RequestPayload = RequestPayload> = {
  method: Method
  data: T
  replace: boolean
  preserveScroll: PreserveStateOption
  preserveState: PreserveStateOption
  only: Array<string>
  except: Array<string>
  headers: Record<string, string>
  errorBag: string | null
  forceFormData: boolean
  queryStringArrayFormat: QueryStringArrayFormatOption
  async: boolean
  showProgress: boolean
  prefetch: boolean
  fresh: boolean
  reset: string[]
  preserveUrl: boolean
  invalidateCacheTags: string | string[]
  viewTransition: boolean | ((viewTransition: ViewTransition) => void)
}

export type GlobalEventsMap<T extends RequestPayload = RequestPayload> = {
  before: {
    parameters: [PendingVisit<T>]
    details: {
      visit: PendingVisit<T>
    }
    result: boolean | void
  }
  start: {
    parameters: [PendingVisit<T>]
    details: {
      visit: PendingVisit<T>
    }
    result: void
  }
  progress: {
    parameters: [Progress | undefined]
    details: {
      progress: Progress | undefined
    }
    result: void
  }
  finish: {
    parameters: [ActiveVisit<T>]
    details: {
      visit: ActiveVisit<T>
    }
    result: void
  }
  cancel: {
    parameters: []
    details: {}
    result: void
  }
  beforeUpdate: {
    parameters: [Page]
    details: {
      page: Page
    }
    result: void
  }
  navigate: {
    parameters: [Page]
    details: {
      page: Page
    }
    result: void
  }
  success: {
    parameters: [Page]
    details: {
      page: Page
    }
    result: void
  }
  error: {
    parameters: [Errors]
    details: {
      errors: Errors
    }
    result: void
  }
  invalid: {
    parameters: [AxiosResponse]
    details: {
      response: AxiosResponse
    }
    result: boolean | void
  }
  exception: {
    parameters: [Error]
    details: {
      exception: Error
    }
    result: boolean | void
  }
  prefetched: {
    parameters: [AxiosResponse, ActiveVisit<T>]
    details: {
      response: AxiosResponse
      fetchedAt: number
      visit: ActiveVisit<T>
    }
    result: void
  }
  prefetching: {
    parameters: [ActiveVisit<T>]
    details: {
      visit: ActiveVisit<T>
    }
    result: void
  }
}

export type PageEvent = 'newComponent' | 'firstLoad'

export type GlobalEventNames<T extends RequestPayload = RequestPayload> = keyof GlobalEventsMap<T>

export type GlobalEvent<
  TEventName extends GlobalEventNames<T>,
  T extends RequestPayload = RequestPayload,
> = CustomEvent<GlobalEventDetails<TEventName, T>>

export type GlobalEventParameters<
  TEventName extends GlobalEventNames<T>,
  T extends RequestPayload = RequestPayload,
> = GlobalEventsMap<T>[TEventName]['parameters']

export type GlobalEventResult<
  TEventName extends GlobalEventNames<T>,
  T extends RequestPayload = RequestPayload,
> = GlobalEventsMap<T>[TEventName]['result']

export type GlobalEventDetails<
  TEventName extends GlobalEventNames<T>,
  T extends RequestPayload = RequestPayload,
> = GlobalEventsMap<T>[TEventName]['details']

export type GlobalEventTrigger<TEventName extends GlobalEventNames<T>, T extends RequestPayload = RequestPayload> = (
  ...params: GlobalEventParameters<TEventName, T>
) => GlobalEventResult<TEventName, T>

export type GlobalEventCallback<TEventName extends GlobalEventNames<T>, T extends RequestPayload = RequestPayload> = (
  ...params: GlobalEventParameters<TEventName, T>
) => GlobalEventResult<TEventName, T>

export type InternalEvent = 'missingHistoryItem' | 'loadDeferredProps'

export type VisitCallbacks<T extends RequestPayload = RequestPayload> = {
  onCancelToken: CancelTokenCallback
  onBefore: GlobalEventCallback<'before', T>
  onBeforeUpdate: GlobalEventCallback<'beforeUpdate', T>
  onStart: GlobalEventCallback<'start', T>
  onProgress: GlobalEventCallback<'progress', T>
  onFinish: GlobalEventCallback<'finish', T>
  onCancel: GlobalEventCallback<'cancel', T>
  onSuccess: GlobalEventCallback<'success', T>
  onError: GlobalEventCallback<'error', T>
  onPrefetched: GlobalEventCallback<'prefetched', T>
  onPrefetching: GlobalEventCallback<'prefetching', T>
}

export type VisitOptions<T extends RequestPayload = RequestPayload> = Partial<Visit<T> & VisitCallbacks<T>>

export type ReloadOptions<T extends RequestPayload = RequestPayload> = Omit<
  VisitOptions<T>,
  'preserveScroll' | 'preserveState'
>

export type PollOptions = {
  keepAlive?: boolean
  autoStart?: boolean
}

export type VisitHelperOptions<T extends RequestPayload = RequestPayload> = Omit<VisitOptions<T>, 'method' | 'data'>

export type RouterInitParams<ComponentType = Component> = {
  initialPage: Page
  resolveComponent: PageResolver
  swapComponent: PageHandler<ComponentType>
}

export type PendingVisitOptions = {
  url: URL
  completed: boolean
  cancelled: boolean
  interrupted: boolean
}

export type PendingVisit<T extends RequestPayload = RequestPayload> = Visit<T> & PendingVisitOptions

export type ActiveVisit<T extends RequestPayload = RequestPayload> = PendingVisit<T> & Required<VisitOptions<T>>

export type InternalActiveVisit = ActiveVisit & {
  onPrefetchResponse?: (response: Response) => void
  onPrefetchError?: (error: Error) => void
  deferredProps?: boolean
}

export type VisitId = unknown
export type Component = unknown

type FirstLevelOptional<T> = {
  [K in keyof T]?: T[K] extends object ? { [P in keyof T[K]]?: T[K][P] } : T[K]
}

interface CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig> {
  resolve: TComponentResolver
  setup: (options: TSetupOptions) => TSetupReturn
  title?: HeadManagerTitleCallback
  defaults?: FirstLevelOptional<InertiaAppConfig & TAdditionalInertiaAppConfig>
}

export interface CreateInertiaAppOptionsForCSR<
  SharedProps extends PageProps,
  TComponentResolver,
  TSetupOptions,
  TSetupReturn,
  TAdditionalInertiaAppConfig,
> extends CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig> {
  id?: string
  page?: Page<SharedProps>
  progress?:
    | false
    | {
        delay?: number
        color?: string
        includeCSS?: boolean
        showSpinner?: boolean
      }
  render?: undefined
}

export interface CreateInertiaAppOptionsForSSR<
  SharedProps extends PageProps,
  TComponentResolver,
  TSetupOptions,
  TSetupReturn,
  TAdditionalInertiaAppConfig,
> extends CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig> {
  id?: undefined
  page: Page<SharedProps>
  progress?: undefined
  render: unknown
}

export type InertiaAppSSRResponse = { head: string[]; body: string }
export type InertiaAppResponse = Promise<InertiaAppSSRResponse | void>

export type HeadManagerTitleCallback = (title: string) => string
export type HeadManagerOnUpdateCallback = (elements: string[]) => void
export type HeadManager = {
  forceUpdate: () => void
  createProvider: () => {
    preferredAttribute: () => 'data-inertia' | 'inertia'
    reconnect: () => void
    update: HeadManagerOnUpdateCallback
    disconnect: () => void
  }
}

export type LinkPrefetchOption = 'mount' | 'hover' | 'click'

export type TimeUnit = 'ms' | 's' | 'm' | 'h' | 'd'
export type CacheForOption = number | `${number}${TimeUnit}` | string

export type PrefetchOptions = {
  cacheFor: CacheForOption | CacheForOption[]
  cacheTags: string | string[]
}

export type InertiaAppConfig = {
  form: {
    recentlySuccessfulDuration: number
    forceIndicesArrayFormatInFormData: boolean
  }
  // experimental: {
  //   /* not guaranteed */
  // }
  future: {
    /* planned defaults */
    preserveEqualProps: boolean
    useDataInertiaHeadAttribute: boolean
    useDialogForErrorModal: boolean
    useScriptElementForInitialPage: boolean
  }
  prefetch: {
    cacheFor: CacheForOption | CacheForOption[]
    hoverDelay: number
  }
  visitOptions?: (href: string, options: VisitOptions) => VisitOptions
}

export interface LinkComponentBaseProps
  extends Partial<
    Pick<
      Visit<RequestPayload>,
      | 'data'
      | 'method'
      | 'replace'
      | 'preserveScroll'
      | 'preserveState'
      | 'preserveUrl'
      | 'only'
      | 'except'
      | 'headers'
      | 'queryStringArrayFormat'
      | 'async'
      | 'viewTransition'
    > &
      VisitCallbacks & {
        href: string | UrlMethodPair
        prefetch: boolean | LinkPrefetchOption | LinkPrefetchOption[]
        cacheFor: CacheForOption | CacheForOption[]
        cacheTags: string | string[]
      }
  > {}

type PrefetchObject = {
  params: ActiveVisit
  response: Promise<Response>
}

export type InFlightPrefetch = PrefetchObject & {
  staleTimestamp: null
  inFlight: true
}

export type PrefetchCancellationToken = {
  isCancelled: boolean
  cancel: () => void
}

export type PrefetchedResponse = PrefetchObject & {
  staleTimestamp: number
  timestamp: number
  singleUse: boolean
  inFlight: false
  tags: string[]
}

export type PrefetchRemovalTimer = {
  params: ActiveVisit
  timer: number
}

export type ProgressSettings = {
  minimum: number
  easing: string
  positionUsing: 'translate3d' | 'translate' | 'margin'
  speed: number
  trickle: boolean
  trickleSpeed: number
  showSpinner: boolean
  barSelector: string
  spinnerSelector: string
  parent: string
  template: string
  includeCSS: boolean
  color: string
}

export type UrlMethodPair = { url: string; method: Method }

export type FormComponentOptions = Pick<
  VisitOptions,
  'preserveScroll' | 'preserveState' | 'preserveUrl' | 'replace' | 'only' | 'except' | 'reset'
>

export type FormComponentProps = Partial<
  Pick<Visit, 'headers' | 'queryStringArrayFormat' | 'errorBag' | 'showProgress' | 'invalidateCacheTags'> &
    Omit<VisitCallbacks, 'onPrefetched' | 'onPrefetching'>
> & {
  method?: Method | Uppercase<Method>
  action?: string | UrlMethodPair
  transform?: (data: Record<string, FormDataConvertible>) => Record<string, FormDataConvertible>
  options?: FormComponentOptions
  onSubmitComplete?: (props: FormComponentonSubmitCompleteArguments) => void
  disableWhileProcessing?: boolean
  resetOnSuccess?: boolean | string[]
  resetOnError?: boolean | string[]
  setDefaultsOnSuccess?: boolean
}

export type FormComponentMethods = {
  clearErrors: (...fields: string[]) => void
  resetAndClearErrors: (...fields: string[]) => void
  setError(field: string, value: ErrorValue): void
  setError(errors: Record<string, ErrorValue>): void
  reset: (...fields: string[]) => void
  submit: () => void
  defaults: () => void
  getData: () => Record<string, FormDataConvertible>
  getFormData: () => FormData
}

export type FormComponentonSubmitCompleteArguments = Pick<FormComponentMethods, 'reset' | 'defaults'>

export type FormComponentState = {
  errors: Record<string, ErrorValue>
  hasErrors: boolean
  processing: boolean
  progress: Progress | null
  wasSuccessful: boolean
  recentlySuccessful: boolean
  isDirty: boolean
}

export type FormComponentSlotProps = FormComponentMethods & FormComponentState

export type FormComponentRef = FormComponentSlotProps

export interface UseInfiniteScrollOptions {
  // Core data
  getPropName: () => string
  inReverseMode: () => boolean
  shouldFetchNext: () => boolean
  shouldFetchPrevious: () => boolean
  shouldPreserveUrl: () => boolean

  // Elements
  getTriggerMargin: () => number
  getStartElement: () => HTMLElement
  getEndElement: () => HTMLElement
  getItemsElement: () => HTMLElement
  getScrollableParent: () => HTMLElement | null

  // Callbacks
  onBeforePreviousRequest: () => void
  onBeforeNextRequest: () => void
  onCompletePreviousRequest: () => void
  onCompleteNextRequest: () => void
}

export interface UseInfiniteScrollDataManager {
  getLastLoadedPage: () => number | string | null
  getPageName: () => string
  getRequestCount: () => number
  hasPrevious: () => boolean
  hasNext: () => boolean
  fetchNext: (reloadOptions?: ReloadOptions) => void
  fetchPrevious: (reloadOptions?: ReloadOptions) => void
  removeEventListener: () => void
}

export interface UseInfiniteScrollElementManager {
  setupObservers: () => void
  enableTriggers: () => void
  disableTriggers: () => void
  refreshTriggers: () => void
  flushAll: () => void
  processManuallyAddedElements: () => void
  processServerLoadedElements: (loadedPage: string | number | null) => void
}

export interface UseInfiniteScrollProps {
  dataManager: UseInfiniteScrollDataManager
  elementManager: UseInfiniteScrollElementManager
  flush: () => void
}

export interface InfiniteScrollSlotProps {
  loading: boolean
  loadingPrevious: boolean
  loadingNext: boolean
}

export interface InfiniteScrollActionSlotProps {
  loading: boolean
  loadingPrevious: boolean
  loadingNext: boolean
  fetch: () => void
  autoMode: boolean
  manualMode: boolean
  hasMore: boolean
  hasPrevious: boolean
  hasNext: boolean
}

export interface InfiniteScrollRef {
  fetchNext: (reloadOptions?: ReloadOptions) => void
  fetchPrevious: (reloadOptions?: ReloadOptions) => void
  hasPrevious: () => boolean
  hasNext: () => boolean
}

export interface InfiniteScrollComponentBaseProps {
  data: string
  buffer?: number
  as?: string
  manual?: boolean
  manualAfter?: number
  preserveUrl?: boolean
  reverse?: boolean
  autoScroll?: boolean
  onlyNext?: boolean
  onlyPrevious?: boolean
}

declare global {
  interface DocumentEventMap {
    'inertia:before': GlobalEvent<'before'>
    'inertia:start': GlobalEvent<'start'>
    'inertia:progress': GlobalEvent<'progress'>
    'inertia:success': GlobalEvent<'success'>
    'inertia:error': GlobalEvent<'error'>
    'inertia:invalid': GlobalEvent<'invalid'>
    'inertia:exception': GlobalEvent<'exception'>
    'inertia:finish': GlobalEvent<'finish'>
    'inertia:beforeUpdate': GlobalEvent<'beforeUpdate'>
    'inertia:navigate': GlobalEvent<'navigate'>
  }
}
